<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
  </style>
</head>
<body>
  
  <script>
    /* 
      1. 创建对象

      面向对象编程：探索如何通过构造函数模拟对象类，以及如何创建对象

      对象包，也叫做命名空间，它储存着对象的数据，使得数据的组织和访问变得更容易；
      对象也常做数据储存体，用于在网络上运输数据

      两个步骤：
        第一步定义对象特质的模板，也成为类，（抽象的过程：为了使我们编程的目标而利用事物的一些重要特性去复杂的事物简单化）
        第二步当一个对象需要从类中创建出来时，类的构造函数就会运行来创建这个实例（实例化，实例对象被类实例化）

      js 使用称为构造函数的特殊函数来定义对象和它们的特征





      原型和原型链
      js 常被描述为一个基于原型的语言，即每个对象都有原型对象。
      对象以其原型为模板，从原型中继承方法和属性；原型对象也可能拥有原型，从中继承属性和方法，一层一层，以此类推，这种关系被称为原型链（prototype chain）

      原型链解释了为何一个对象会拥有定义在其他对象中的属性和方法，准确的说，这些属性和方法定义在 Object 的构造函数之上的 prototype 属性上，而非对象实例本身

      注意：对象的原型可以通过 Object.getPrototypeOf(obj) 或者 __proto__属性（已弃用）获得；是每个实例上都有的属性；
          而构造函数的 prototype 属性是构造函数的属性；也就是说 Object.getPrototypeOf(new Func()) 和 Func.prototype 指向同一个对象

      在 js 中，每个函数都会有一个特殊属性，叫做原型（prototype）

      默认原型（prototype）上有两个属性：
        1. constructor，指向创建当前实例的构造函数
        2. [[prototype]]，标准中的原型对象，非标准的是 __proto__

      原型对象上的属性或方法是所有实例对象共享的

      一个对象的属性或方法的查找规则：首先在当前对象上查找是否有这个属性或方法，如果存在则直接返回；如果不存在，就去当前对象的原型对象上去查找（当前对象的 __proto__ 属性上，也就是创建当前对象的构造函数的 prototype 上），
          如果在当前对象的原型对象上也没找到，再去当前对象的原型对象的原型对象上去查找，还是没找到，最终会查找到 Object.prototype（所有函数的 prototype 的 __proto__ 最终都指向 Object.prototype，而 Object.prototype 的 __proto__ 是 null），还是没有则返回 undefined
      一个对象的属性或方法的设置规则：如果当前对象上没有要设置的属性或方法，则直接添加；如果当前对象上存在了要设置的属性或方法，则直接覆盖


      Object.create(): 以某个对象为原型来创建一个对象
     */
    
    // 创建对象
    const person = {
      name: 'mike',
      greeting: function() {
        console.log(`my name is ${this.name}`)
      }
    }
  </script>

  <script>
    /* 普通函数创建一个人对象 */
    function createNewPerson(name) {
      const obj = {};
      obj.name = name;
      obj.greeting = function() {
        console.log(`my name is ${this.name}`)
      }
      return obj;
    }
    const obj01 = createNewPerson('mike')
    console.log(obj01.name)
    console.log(obj01.greeting())


    /* 1. 构造函数创建对象类 */
    function Person(name) {
      // this 指的是当前对象
      this.name = name;
      this.greeting = function() {
        console.log(`my name is ${this.name}`)
      }
    }
    // 2. 使用 new 实例化
    const obj02 = new Person('mike')
    console.log(obj02.name)
    console.log(obj02.greeting())

    /* 以上方式有个问题，就是当每次创建新对象时，都会新建一个 greeting 函数，这样当创建很多对象时，会浪费不必要的内存
       解决方案：将对象的共有函数放在原型上
     */
  </script>


  <script>
    // 查看一个函数的原型
    function func() {}
    console.log(func.prototype)

    // 添加一些属性到函数原型上
    func.prototype.name = 'mike'
  </script>


  <!-- <script>
    /*
      第一种方式，原型链继承
      核心：将父类实例做为子类的原型
        - 由于方法定义在父类的原型上，复用了父类构造函数的方法，比如say方法
      缺点：
        1. 创建子类实例时，不能传父类的参数
        2. 子类实例共享了父类构造函数的引用属性，比如 arr 属性（子类的所有实例化对象继承的是同一个父类的对象，他们指向的都是同一块内存）
        3. 不能实现多继承
    */
    function Parent(name) {
      // 该属性，强调私有
      this.name = name || '父亲'
      this.arr = [1]
    }
    Parent.prototype.say = function () {
      console.log('my name is :', name)
    }
    function Child(like) {
      this.like = like
    }
    // 核心，修改了子类的原型，但此时 Child.prototype.constructor === Parent
    Child.prototype = new Parent()
    Child.prototype.constructor = Child
    const boy1 = new Child()
    const boy2 = new Child()
    // 优点是共享了父类构造函数的 say 方法
    console.log(boy1.say(), boy2.say(), boy1.say === boy2.say)
    // 缺点1，不能向父类构造函数传参
    console.log(boy1.name, boy2.name, boy1.name === boy2.name)
    // 缺点2，子类实例共享了父类构造函数的引用属性，比如 arr 属性
    boy1.arr.push(2)
    console.log('boy2.arr: ', boy2.arr)
  </script> -->


  <!-- <script>
    /*
      第二种方式，借用构造函数
      核心：等于是借用父类的构造函数来增强子类的实例，等于是复制父类的实例属性给子类
      优点：实例之间独立
        1. 创建子类实例，可以向父类构造函数传参数
        2. 子类实例不共享父类构造函数的引用属性，
        3. 可实现多继承，（通过多个 call 或者 apply 继承多个类）
      缺点：
        1. 子类实例继承不了父类原型上的属性，因为没有用到原型
        2. 父类的方法不能复用（子类实例继承不了父类原型上的属性，所以方法只能在父类构造函数中定义，导致方法不能复用）
    */
    function Parent(name) {
      this.name = name
      this.arr = [1]
      this.say = function() {
        console.log(`my name is ${this.name}`)
      }
    }
    function Child(name, like) {
      Parent.call(this, name)
      this.like = like
    }
    const boy1 = new Child('mike', 'apple')
    const boy2 = new Child('jane', 'orange')
    // 优点1：可向父类构造函数传参
    console.log(boy1.name, boy2.name)
    // 优点2：不共享父类构造函数的引用属性
    boy1.arr.push(2)
    console.log(boy1.arr, boy2.arr)
    // 缺点1：方法不能复用
    console.log(boy1.say === boy2.say)  // false
    // 缺点2：不能继承父类原型上的方法
    Parent.prototype.walk = function() {
      console.log('i can walk!')
    }
    boy1.walk  // undefined
  </script> -->


  <!-- <script>
    /*
      第三种方式，组合继承
      核心：通过调用父类构造函数，来实现继承父类的属性和方法并保留传参的优点；然后通过将父类实例做为子类原型，实现函数复用
      优点：
        1. 保留构造函数的优点：创建子类实例，可以向父类构造函数传参数
        2. 保留原型链的优点：父类的方法定义在父类的原型上，可实现方法的复用
        3. 不共享父类的引用属性
      缺点：
        1. 由于调用了两次父类的构造函数，会存在一份多余的父类实例属性

      注意：组合继承的方式，需要修复 Child.prototype.constructor 的指向
    */
    // 第一次 Parent.call(this), 从父类拷贝一份实例属性，做为子类的实例属性；第二次 Parent.prototype = new Parent(), 创建父类实例做为子类原型
    //     Child.prototype中的父类属性和方法会被第一次拷贝来的实例属性屏蔽掉，所以多余
    function Parent(name) {
      this.name = name
      this.arr = [1]
    }
    Parent.prototype.say = function() {
      console.log(`my name is ${this.name}`)
    }
    function Child(name, like) {
      // 核心 第一次
      Parent.call(this, name)
      this.like = like
    }
    // 核心 第二次
    Child.prototype = new Parent()
    // 修正 constructor 指向
    Child.prototype.constructor = Child
    // 测试
    let boy1 = new Child('mike', 'apple')
    let boy2 = new Child('jane', 'orange')
    // 优点1，可向父类构造函数传参
    console.log(boy1.name, boy1.like)
    // 优点2，可复用父类原型上的方法
    console.log(boy1.say === boy2.say)  // true
    // 优点3，不共享父类的引用属性，如果 arr 属性
    boy1.arr.push(2)
    console.log(boy1.arr, boy2.arr)
    console.log(Child.prototype.__proto__ === Parent.prototype)
  </script> -->



  <script>
    /*
      第四种方式：组合继承优化1
      核心：通过这种方式，砍掉父类的实例对象，这样在调用父类的构造函数时，就不会初始化两次实例，避免组合继承的确定
      优点：
        1. 只调用一次父类构造函数
        2. 保留构造函数的优点：父类的实例方法定义在父类的原型对象上，可以实现方法复用
        3. 保留原型链的优点：父类的实例方法在父类的原型对象上，可以实现方法复用
      缺点：
        修改构造函数的指向后，父类实例的构造函数指向，同时也发生变化
      注意：这种方式要记得修复 Child.prototype.constructor 指向，原因是不能判断子类实例的直接构造函数，到底是子类构造函数还是父类构造函数
    */
    // function Parent(name) {
    //   this.name = name
    //   this.arr = [1]
    // }
    // Parent.prototype.say = function() {
    //   console.log(`my name is ${this.name}`)
    // }
    // function Child(name, like) {
    //   Parent.call(this, name)
    //   this.like = like
    // }
    // Child.prototype = Parent.prototype
    // Child.prototype.constructor = Child



    // 完美方案
    function Parent(name) {
      this.name = name
      this.arr = [1]
    }
    Parent.prototype.say = function() {
      console.log(`my name is ${this.name}`)
    }
    function Child(name, like) {
      Parent.call(this, name)
      this.like = like
    }
    Child.prototype = Object.create(Parent.prototype)
    Child.prototype.constructor = Child

    Child.prototype = {}
    Object.setPrototypeOf(Child.prototype, Parent.prototype)
    Child.prototype.constructor = Child
  </script>
</body>
</html>